﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using Expect.Core.Interfaces;

namespace Expect.Core.BaseClasses
{
    using Expect.Core.Errors;

    /// <summary>
    /// Default implementation of the fixture catalog
    /// </summary>
    public class DefaultFixtureCatalog : IExpectCatalog
    {
        private List<string> importedAssemblies = new List<string>();

        private Dictionary<string, Type> FixtureCatalog = new Dictionary<string, Type>();

        private Dictionary<string, Type> _containers = new Dictionary<string, Type>();

        private Dictionary<Type, IExpectTypeConverter> _converters = new Dictionary<Type, IExpectTypeConverter>();

        /// <summary>
        /// Name provider for catalog
        /// </summary>
        public INameProvider NameProvider { get; set; }

        /// <summary>
        /// Constructor
        /// </summary>
        public DefaultFixtureCatalog(INameProvider nameProvider)
        {
            NameProvider = nameProvider;
            //LoadFixtureCatalog();
        }

        /// <summary>
        /// Get the type of the object given it's class name
        /// </summary>
        /// <param name="fixturename"></param>
        /// <returns></returns>
        public Type GetFixture(string fixturename)
        {
            if (FixtureCatalog.ContainsKey(fixturename))
            {
                var res = FixtureCatalog[fixturename];
                if (res.IsAFixtureImplementation()) 
                    return res;
            }
            return null;
        }

        /// <summary>
        /// Get the type of the object given it's class name
        /// </summary>
        /// <param name="specificationname"></param>
        /// <returns></returns>
        public Type GetSpecification(string specificationname)
        {
            if (FixtureCatalog.ContainsKey(specificationname))
            {
                var res = FixtureCatalog[specificationname];
                if (res.IsASpecificationImplementation())
                    return res;
            }
            return null;
        }

        private void LoadFixtureCatalog()
        {
            Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();

            foreach (var ass in assemblies)
            {
                ImportTypes(ass);
            }
        }

        /// <summary>
        /// Import types from the assembly
        /// </summary>
        /// <param name="ass">assembly to scan</param>
        public void ImportTypes(Assembly ass)
        {
            try
            {
                if (importedAssemblies.Contains(ass.FullName)) return;

                importedAssemblies.Add(ass.FullName);
                Type[] types = ass.GetTypes();
                foreach (var type in types.Where(t => !t.IsAbstract))
                {
                    if (type.IsAFixtureImplementation())
                    {
                        this.AddToCatalog(type);
                    }
                    if (type.IsASpecificationImplementation())
                    {
                        this.AddToCatalog(type);
                    }
                    if (type.IsATypeConverter())
                    {
                        RegisterTypeConverter(type);
                    }
                    if (type.IsASpecificationContainer())
                    {
                        RegisterContainer(type);
                    }
                }
            }
            catch (Exception ex)
            {
                var exc = ex;
                //logger.Warn("Fixture Catalog error", ex);
            }
        }

        private void AddToCatalog(Type t)
        {
            this.FixtureCatalog.Add(t.Name, t);

            var hints = t.GetCustomAttributes(typeof(FixtureHintAttribute), true);
            if (hints != null && hints.Length > 0)
            {
                foreach (FixtureHintAttribute hint in hints)
                {
                    string[] tokens = hint.Name.Split('|');
                    FixtureRow row = new FixtureRow(tokens);
                    var name = NameProvider.ElementName(row.NamesForFluent().ToArray());
                    if (!FixtureCatalog.ContainsKey(name))
                        FixtureCatalog.Add(name, t);
                }
            }
        }

        private void RegisterTypeConverter(Type type)
        {
            var interfaces = type.GetInterfaces();
            
            var typeConverterInterface = interfaces.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IExpectTypeConverter<>)).FirstOrDefault();
            if (typeConverterInterface != null && typeConverterInterface.IsGenericType)
            {
                var targetType = typeConverterInterface.GetGenericArguments()[0];
                var converterInstance = Activator.CreateInstance(type);
                _converters.Add(targetType, (IExpectTypeConverter)converterInstance);
            }
        }

        private void RegisterContainer(Type type)
        {
            var attrs = (SpecificationContainerAttribute[])type.GetCustomAttributes(typeof(SpecificationContainerAttribute), false);
            if (attrs != null && attrs.Length == 1)
            {
                _containers.Add(attrs[0].Path.ToLower(), type);
            }
        }

        /// <summary>
        /// Indicate if the catalog has registered a type converter
        /// </summary>
        /// <param name="targetType"></param>
        /// <returns></returns>
        public bool HasConverterFor(Type targetType)
        {
            return _converters.ContainsKey(targetType);
        }

        /// <summary>
        /// Convert a string to target type
        /// </summary>
        /// <param name="targetType">target type</param>
        /// <param name="val">string representation</param>
        /// <returns>target type instance</returns>
        public object GetValue(Type targetType, string val)
        {
            try
            {
                if (_converters.ContainsKey(targetType))
                {
                    return _converters[targetType].Convert(val);
                }
                
                if (targetType.IsEnum)
                {
                    return Enum.Parse(targetType, val);
                }
                
                return Convert.ChangeType(val, targetType);                
            }
            catch (InvalidCastException ex)
            {
                string errorformat = "No type conversion found for {0}, please add a class implementing {1}<{0}>";
                throw new ArgumentException(string.Format(errorformat, targetType.FullName, typeof(IExpectTypeConverter).Name), ex);
            }
        }

        /// <summary>
        /// Get a named specification container
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public ISpecificationContainer GetContainerFor(string name)
        {
            if (!name.StartsWith("\\")) name = "\\" + name;
            if (!name.EndsWith(".expect", StringComparison.OrdinalIgnoreCase)) name = name + ".expect";
            name = name.ToLower();

            if (!_containers.ContainsKey(name))
                throw new InvalidOperationException("Setup container " + name + " not found");
            
            Type containerType = _containers[name];
            ISpecificationContainer container = (ISpecificationContainer)Activator.CreateInstance(containerType);
            return container;
        }
    }
}
